package com.dimples.dd.log.core.aspect;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.dimples.dd.common.exception.BizException;
import com.dimples.dd.common.pojo.PageResult;
import com.dimples.dd.common.result.CommonResult;
import com.dimples.dd.common.result.ResultCode;
import com.dimples.dd.common.util.json.JsonUtils;
import com.dimples.dd.common.util.monitor.TracerUtils;
import com.dimples.dd.common.util.servlet.ServletUtils;
import com.dimples.dd.log.core.annotation.ApiLog;
import com.dimples.dd.log.core.enums.OperateTypeEnum;
import com.dimples.dd.system.api.logger.OperateLogFeign;
import com.dimples.dd.system.api.logger.dto.OperateLogCreateReqDTO;
import com.dimples.dd.web.core.util.WebFrameworkUtils;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * 操作日志记录处理
 *
 * @author zhongyj <1126834403@qq.com><br/>
 * @date 2024/9/5
 */
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class ApiLogAspect {

    /**
     * 排除敏感属性字段
     */
    public static final String[] EXCLUDE_SANITIZE_KEYS = {"password", "oldPassword", "newPassword", "confirmPassword", "token", "accessToken", "refreshToken"};

    private static final String LOG_CONTENT = "[ 方法 ] ==> %s \n[ 参数 ] ==> %s \n[ I P ] ==> %s \n[ 时间 ] ==> %s ";

    /**
     * 计算操作消耗时间
     */
    private static final ThreadLocal<LocalDateTime> TIME_THREADLOCAL = new NamedThreadLocal<>("Cost Time");

    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    private final OperateLogFeign operateLogFeign;

    /**
     * 处理请求前执行
     */
    @Before(value = "@annotation(controllerLog)")
    public void boBefore(JoinPoint joinPoint, ApiLog controllerLog) {
        TIME_THREADLOCAL.set(LocalDateTime.now());
    }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "@annotation(logAnnotation)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, ApiLog logAnnotation, Object jsonResult) {
        handleLog(joinPoint, logAnnotation, null, jsonResult);
    }

    /**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param e         异常
     */
    @AfterThrowing(value = "@annotation(logAnnotation)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, ApiLog logAnnotation, Exception e) {
        handleLog(joinPoint, logAnnotation, e, null);
    }

    protected void handleLog(final JoinPoint joinPoint, ApiLog logAnnotation, final Exception ex, Object jsonResult) {
        // *========数据库日志=========*//
        HttpServletRequest request = ServletUtils.getRequest();
        if (request == null) {
            return;
        }

        try {
            // 正常执行，记录日志
            createApiLog(request, joinPoint, logAnnotation, ex, jsonResult);
        } catch (Exception exp) {
            // 记录本地异常日志
            log.error("异常信息:{}", ExceptionUtil.stacktraceToString(exp));
        } finally {
            TIME_THREADLOCAL.remove();
        }
    }

    private void createApiLog(HttpServletRequest request, JoinPoint joinPoint, ApiLog logAnnotation, Exception ex, Object jsonResult) {
        OperateLogCreateReqDTO operateLogCreateReqDTO = new OperateLogCreateReqDTO();
        try {
            // 1. 生成日志数据
            boolean enable = buildApiLog(operateLogCreateReqDTO, joinPoint, logAnnotation, request, ex, jsonResult);
            // 2. 打印 request 日志
            if (!Objects.equals("prod", SpringUtil.getActiveProfile()) && enable) {

                log.info(StrUtil.format("\n =============================== {} ========================================\n" +
                                "[ 用时 ] ==> {} 毫秒 \n" +
                                "[ 类型 ] ==> {} \n" +
                                "{} \n" +
                                "======================================================================================",
                        operateLogCreateReqDTO.getOperateName(), operateLogCreateReqDTO.getDuration(), OperateTypeEnum.getDesc(operateLogCreateReqDTO.getOperateType()),
                        String.format(LOG_CONTENT,
                                operateLogCreateReqDTO.getJavaMethod(),
                                StrUtil.isAllBlank(operateLogCreateReqDTO.getRequestParams(), operateLogCreateReqDTO.getRequestBody())
                                        ? "" : Objects.requireNonNullElse(operateLogCreateReqDTO.getRequestParams(), operateLogCreateReqDTO.getRequestBody()),
                                operateLogCreateReqDTO.getUserIp(),
                                DateUtil.now())));
            }
            if (enable) {
                // 3. 异步保存数据库
                ThreadUtil.execAsync(() -> this.operateLogFeign.createOperateLog(operateLogCreateReqDTO), true);
            }
        } catch (Throwable th) {
            log.error("[createApiLog][url({}) log({}) 发生异常]", request.getRequestURI(), JsonUtils.toJsonString(operateLogCreateReqDTO), th);
        }
    }

    private boolean buildApiLog(OperateLogCreateReqDTO logCreateReq, JoinPoint joinPoint, ApiLog logAnnotation, HttpServletRequest request, Exception ex, Object jsonResult) {
        if (logAnnotation == null) {
            return false;
        }
        // 判断：是否要记录操作日志
        if (BooleanUtil.isFalse(logAnnotation.enable())) {
            return false;
        }

        // 处理用户信息
        logCreateReq.setUserId(WebFrameworkUtils.getLoginUserId())
                .setUserType(WebFrameworkUtils.getLoginUserType(request).byteValue());
        // 设置访问结果
        if (jsonResult != null) {
            if (jsonResult instanceof CommonResult<?> result) {
                logCreateReq.setResultCode(result.getCode()).setResultMsg(result.getMsg());
            } else if (jsonResult instanceof PageResult<?> result) {
                logCreateReq.setResultCode(result.getCode()).setResultMsg(result.getMsg());
            }
        } else if (ex != null) {
            if (ex instanceof BizException) {
                logCreateReq.setResultCode(((BizException) ex).getCode());
            } else {
                logCreateReq.setResultCode(ResultCode.SYSTEM_EXECUTION_ERROR.getCode());
            }
            logCreateReq.setResultMsg(ExceptionUtil.getRootCauseMessage(ex));
        } else {
            logCreateReq.setResultCode(ResultCode.SUCCESS.getCode()).setResultMsg("");
        }
        // 设置方法名称
        Class<?> aClass = joinPoint.getTarget().getClass();
        String className = aClass.getName();
        String methodName = joinPoint.getSignature().getName();

        Object[] args = joinPoint.getArgs();
        String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
        List<String> tmpParam = CollUtil.newArrayList();
        for (String parameterName : parameterNames) {
            List<String> tmp = CollUtil.newArrayList();
            String paramType = StrUtil.toStringOrNull(args[ArrayUtil.indexOf(parameterNames, parameterName)]);
            if (StrUtil.isBlank(paramType)) {
                paramType = "String";
            }
            tmp.add(StrUtil.subBefore(paramType, "(", false));
            tmp.add(parameterName);
            tmpParam.add(StrUtil.join(StrUtil.SPACE, tmp));
        }
        logCreateReq.setJavaMethod(className + "." + methodName + "(" + StrUtil.join(StrUtil.COMMA, tmpParam) + ")");
        // 设置请求字段
        logCreateReq.setTraceId(TracerUtils.getTraceId())
                .setApplicationName(logAnnotation.applicationName())
                .setRequestUrl(request.getRequestURI())
                .setRequestMethod(request.getMethod())
                .setUserAgent(ServletUtils.getBrowser())
                .setUserIp(ServletUtils.getIp());
        String[] sanitizeKeys = logAnnotation.sanitizeKeys();
        Boolean requestEnable = logAnnotation.requestEnable();
        if (!BooleanUtil.isFalse(requestEnable)) { // 默认记录，所以判断 !false
            String requestBody = "";
            Map<String, String> queryString = null;
            if (ServletUtils.isJsonRequest(request)) {
                requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
            } else {
                queryString = ServletUtils.getParamMap(request);
            }
            logCreateReq.setRequestParams(sanitizeMap(queryString, sanitizeKeys));
            logCreateReq.setRequestBody(sanitizeJson(requestBody, sanitizeKeys));
        }
        Boolean responseEnable = logAnnotation.responseEnable();
        if (BooleanUtil.isTrue(responseEnable)) { // 默认不记录，默认强制要求 true
            if (jsonResult != null) {
                logCreateReq.setResponseBody(sanitizeDataJson(JsonUtils.toJsonString(jsonResult), sanitizeKeys));
            }
        }
        // 持续时间
        logCreateReq
                .setBeginTime(DateUtil.format(TIME_THREADLOCAL.get(), DatePattern.NORM_DATETIME_MS_PATTERN))
                .setEndTime(DateUtil.format(LocalDateTime.now(), DatePattern.NORM_DATETIME_MS_PATTERN))
                .setDuration((int) LocalDateTimeUtil.between(
                        DateUtil.parseLocalDateTime(logCreateReq.getBeginTime(), DatePattern.NORM_DATETIME_MS_PATTERN),
                        DateUtil.parseLocalDateTime(logCreateReq.getEndTime(), DatePattern.NORM_DATETIME_MS_PATTERN), ChronoUnit.MILLIS)
                );

        // 操作模块
        try {
            Tag tagAnnotation = aClass.getAnnotation(Tag.class);
            Operation operationAnnotation = aClass.getDeclaredMethod(methodName, ((MethodSignature) joinPoint.getSignature()).getParameterTypes()).getAnnotation(Operation.class);
            String operateModule = StrUtil.isNotBlank(logAnnotation.operateModule()) ? logAnnotation.operateModule() :
                    tagAnnotation != null ? StrUtil.nullToDefault(tagAnnotation.name(), tagAnnotation.description()) : null;
            String operateName = StrUtil.isNotBlank(logAnnotation.operateName()) ? logAnnotation.operateName() :
                    operationAnnotation != null ? operationAnnotation.summary() : null;
            OperateTypeEnum operateType = logAnnotation.operateType().length > 0 ?
                    logAnnotation.operateType()[0] : parseOperateLogType(request);
            logCreateReq.setOperateModule(operateModule).setOperateName(operateName).setOperateType(operateType.getType());
        } catch (NoSuchMethodException e) {
            log.error("日志记录解析方法注解和类注解失败: {}", ExceptionUtil.stacktraceToString(e));
        }

        return true;
    }

    // ========== 解析 @ApiAccessLog、@Swagger 注解  ==========

    private static OperateTypeEnum parseOperateLogType(HttpServletRequest request) {
        RequestMethod requestMethod = RequestMethod.resolve(request.getMethod());
        if (requestMethod == null) {
            return OperateTypeEnum.OTHER;
        }
        return switch (requestMethod) {
            case GET -> OperateTypeEnum.SELECT;
            case POST -> OperateTypeEnum.CREATE;
            case PUT -> OperateTypeEnum.UPDATE;
            case DELETE -> OperateTypeEnum.DELETE;
            default -> OperateTypeEnum.OTHER;
        };
    }

    // ========== 请求和响应的脱敏逻辑，移除类似 password、token 等敏感字段 ==========

    private static String sanitizeMap(Map<String, ?> map, String[] sanitizeKeys) {
        if (CollUtil.isEmpty(map)) {
            return null;
        }
        if (sanitizeKeys != null) {
            MapUtil.removeAny(map, sanitizeKeys);
        }
        MapUtil.removeAny(map, EXCLUDE_SANITIZE_KEYS);
        return JsonUtils.toJsonString(map);
    }

    private static String sanitizeJson(String jsonString, String[] sanitizeKeys) {
        if (StrUtil.isEmpty(jsonString)) {
            return null;
        }
        try {
            JsonNode rootNode = JsonUtils.parseTree(jsonString);
            sanitizeJson(rootNode, sanitizeKeys);
            return JsonUtils.toJsonString(rootNode);
        } catch (Exception e) {
            // 脱敏失败的情况下，直接忽略异常，避免影响用户请求
            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
            return jsonString;
        }
    }

    private static String sanitizeDataJson(String jsonString, String[] sanitizeKeys) {
        try {
            JsonNode rootNode = JsonUtils.parseTree(jsonString);
            if (ObjectUtil.isNotEmpty(rootNode.get("data"))) {
                sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段，不处理 code、msg 字段，避免错误被脱敏掉
            }
            return JsonUtils.toJsonString(rootNode);
        } catch (Exception e) {
            // 脱敏失败的情况下，直接忽略异常，避免影响用户请求
            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
            return jsonString;
        }
    }

    private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) {
        // 情况一：数组，遍历处理
        if (node.isArray()) {
            for (JsonNode childNode : node) {
                sanitizeJson(childNode, sanitizeKeys);
            }
            return;
        }
        // 情况二：非 Object，只是某个值，直接返回
        if (!node.isObject()) {
            return;
        }
        //  情况三：Object，遍历处理
        Iterator<Map.Entry<String, JsonNode>> iterator = node.properties().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, JsonNode> entry = iterator.next();
            if (ArrayUtil.contains(sanitizeKeys, entry.getKey())
                    || ArrayUtil.contains(EXCLUDE_SANITIZE_KEYS, entry.getKey())) {
                iterator.remove();
                continue;
            }
            sanitizeJson(entry.getValue(), sanitizeKeys);
        }
    }

}
